/**
 * REF: https://github.com/WQTeam/web-storage-cache
 */
/* eslint-disable */
import { JSONStringify, JSONParse } from './index'

const MAX_EXPIRE_DATE = new Date('Fri, 31 Dec 9999 23:59:59 UTC')
const DEFAULT_EXPIRE_DATE = MAX_EXPIRE_DATE

function log(msg, type = 'error') {
  const args = [`storage ${type}: `, msg]
  console && console[type].apply(console, args)
}

/**
  * https://github.com/gsklee/ngStorage/blob/master/ngStorage.js#L52
  *
  * When Safari (OS X or iOS) is in private browsing mode, it appears as
  * though localStorage is available, but trying to call .setItem throws an
  * exception below: "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was
  * made to add something to storage that exceeded the quota."
  */
function isSupported(storage) {
  let supported = false
  if (storage && storage.setItem) {
    supported = true
    var key = '__' + Math.round(Math.random() * 1e7)
    try {
      storage.setItem(key, key)
      storage.removeItem(key)
    } catch (err) {
      log(err)
      supported = false
    }
  }
  return supported
}

function getStorage(storage) {
  if (typeof storage === 'string' && window[storage] instanceof Storage) {
    return window[storage]
  }
  return storage
}

function isValidDate(date) {
  return Object.prototype.toString.call(date) === '[object Date]' && isNaN(date.getTime())
}

function getExpireDate(expire, now = new Date()) {
  if (typeof expire === 'number') {
    expire = expire === Infinity ? MAX_EXPIRE_DATE : new Date(now.getTime() + expire * 1000)
  } else if (typeof expire === 'string') {
    expire = new Date(expire)
  }

  if (expire && !isValidDate(expire)) {
    throw new Error('`expires` parameter cannot be converted to a valid Date instance')
  }

  return expire
}

// http://crocodillon.com/blog/always-catch-localstorage-security-and-quota-exceeded-errors
function isQuotaExceeded(e) {
  let quotaExceeded = false
  if (!e) return quotaExceeded

  if (e.code) {
    switch (e.code) {
      case 22:
        quotaExceeded = true
        break
      case 1014: // Firefox
        if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
          quotaExceeded = true
        }
        break
    }
  } else if (e.number === -2147024882) { // IE8
    quotaExceeded = true
  }
  return quotaExceeded
}

// cache item constructor
function CacheItemConstructor(value, exp = DEFAULT_EXPIRE_DATE) {
  this.c = new Date().getTime() // current
  this.e = getExpireDate(exp).getTime()
  this.v = value
}

function isCacheItem(item) {
  return item === 'object' && 'c' in item && 'e' in item && 'v' in item
}

function checkCacheItemIfEffective(cacheItem) {
  const now = new Date().getTime()
  return now < cacheItem.e
}

function checkAndWrapKeyAsString(key) {
  if (typeof key !== 'string') {
    warn(key + ' used as a key, but it is not a string.')
    key = String(key)
  }
  return key
}

// cache api
const CacheAPI = {
  set: function(key, value, options) {},
  get: function(key) {},
  delete: function(key) {},
  // Try the best to clean All expires CacheItem.
  deleteAllExpires: function() {},
  // Clear all keys
  clear: function() {},
  // Add key-value item to memcached, success only when the key is not exists in memcached.
  add: function(key, options) {},
  // Replace the key's data item in cache, success only when the key's data item is exists in cache.
  replace: function(key, value, options) {},
  // Set a new options for an existing key.
  touch: function(key, exp) {}
}

// cache api impl
const CacheAPIImpl = {
  set(key, val, options) {
    const _options = { force: true }
    key = checkAndWrapKeyAsString(key)

    // If the parameter is a number, it is treated as an expiration time.
    if (typeof options === 'number') {
      _options.exp = options
    }

    if (val === undefined) {
      return this.delete(key)
    }

    var value = defaultSerializer.serialize(val)

    var cacheItem = new CacheItemConstructor(value, options.exp)
    try {
      this.storage.setItem(key, defaultSerializer.serialize(cacheItem))
    } catch (e) {
      if (_isQuotaExceeded(e)) { // data wasn't successfully saved due to quota exceed so throw an error
        this.quotaExceedHandler(key, value, options, e)
      } else {
        console.error(e)
      }
    }

    return val
  },
  get: function(key) {
    key = _checkAndWrapKeyAsString(key)
    var cacheItem = null
    try {
      cacheItem = defaultSerializer.deserialize(this.storage.getItem(key))
    } catch (e) {
      return null
    }
    if (_isCacheItem(cacheItem)) {
      if (_checkCacheItemIfEffective(cacheItem)) {
        var value = cacheItem.v
        return defaultSerializer.deserialize(value)
      } else {
        this.delete(key)
      }
    }
    return null
  },

  delete: function(key) {
    key = _checkAndWrapKeyAsString(key)
    this.storage.removeItem(key)
    return key
  },

  deleteAllExpires: function() {
    var length = this.storage.length
    var deleteKeys = []
    var _this = this
    for (var i = 0; i < length; i++) {
      var key = this.storage.key(i)
      var cacheItem = null
      try {
        cacheItem = defaultSerializer.deserialize(this.storage.getItem(key))
      } catch (e) {}

      if (cacheItem !== null && cacheItem.e !== undefined) {
        var timeNow = (new Date()).getTime()
        if (timeNow >= cacheItem.e) {
          deleteKeys.push(key)
        }
      }
    }
    deleteKeys.forEach(function(key) {
      _this.delete(key)
    })
    return deleteKeys
  },

  clear: function() {
    this.storage.clear()
  },

  add: function(key, value, options) {
    key = _checkAndWrapKeyAsString(key)
    // If the parameter is a number, it is treated as an expiration time.
    if (typeof options === 'number') {
      options = {
        exp: options
      }
    }
    options = _extend({force: true}, options)
    try {
      var cacheItem = defaultSerializer.deserialize(this.storage.getItem(key))
      if (!_isCacheItem(cacheItem) || !_checkCacheItemIfEffective(cacheItem)) {
        this.set(key, value, options)
        return true
      }
    } catch (e) {
      this.set(key, value, options)
      return true
    }
    return false
  },

  replace: function(key, value, options) {
    key = _checkAndWrapKeyAsString(key)
    var cacheItem = null
    try {
      cacheItem = defaultSerializer.deserialize(this.storage.getItem(key))
    } catch (e) {
      return false
    }
    if (_isCacheItem(cacheItem)) {
      if (_checkCacheItemIfEffective(cacheItem)) {
        this.set(key, value, options)
        return true
      } else {
        this.delete(key)
      }
    }
    return false
  },

  touch: function(key, exp) {
    key = _checkAndWrapKeyAsString(key)
    var cacheItem = null
    try {
      cacheItem = defaultSerializer.deserialize(this.storage.getItem(key))
    } catch (e) {
      return false
    }
    if (_isCacheItem(cacheItem)) {
      if (_checkCacheItemIfEffective(cacheItem)) {
        this.set(key, this.get(key), {exp: exp})
        return true
      } else {
        this.delete(key)
      }
    }
    return false
  }
}

const GlobalStorge = window.Storage

const toString = Object.prototype.toString
const isFunction = fn => toString.call(fn) === '[object Function]'
const isDate = date => toString.call(date) === '[object Date]' && !isNaN(isFunction(date.getTime) && date.getTime())

const Defaults = {
  storage: 'localStorage',
  expire: Infinity
}

export default class {
  constructor(options = {}) {
    this.config = Object.assign(Defaults, options)

    this.storage = null
    this.expire = null
    this.supported = null

    this._init()
  }
  // Private
  _init() {
    const { expire, storage: alias } = this.config

    if (typeof expire !== 'number' && isValidDate(expire)) {
      log(`
        The property 'expire' cannot be converted to a valid Date instance.
        It will reset to be 'Infinity'.
      `)

      this.expire = Infinity
    } else {
      this.expire = expire
    }

    const instance = getStorage(alias)
    this.supported = isSupported(instance)

    if (this.supported) {
      this.storage = instance
    }
  }
  _getStorage(storage) {
    if (typeof storage === 'string' && window[storage] instanceof Storage) {
      return window[storage]
    }
    return storage
  }
}
